Skip to content

feat: include TTY and tmux pane in session list#90

Merged
benvinegar merged 2 commits intomainfrom
feat/session-tty-metadata
Mar 26, 2026
Merged

feat: include TTY and tmux pane in session list#90
benvinegar merged 2 commits intomainfrom
feat/session-tty-metadata

Conversation

@benvinegar
Copy link
Member

Summary

  • Add tty and tmuxPane fields to session registration so hunk session list and hunk session get show which terminal each session belongs to
  • TTY path resolved via the tty command at registration time; tmux pane read from TMUX_PANE env var
  • Fields are optional — omitted when not available (e.g. non-TTY or non-tmux environment)

Closes #89

Test plan

  • Typecheck passes
  • New unit tests: list/get formatters show tty/tmux when present, omit when absent
  • New unit tests: daemon state passes fields through to listed sessions
  • TTY smoke test passes
  • Format check passes

🤖 Generated with Claude Code

benvinegar and others added 2 commits March 23, 2026 15:26
Add tty and tmuxPane fields to HunkSessionRegistration and
ListedSession so `hunk session list` and `hunk session get` can
show which terminal a session belongs to. This makes it easier
to identify sessions when multiple are running.

Closes #89

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@benvinegar
Copy link
Member Author

@greptileai review

@benvinegar benvinegar merged commit a9af393 into main Mar 26, 2026
3 checks passed
@greptile-apps
Copy link

greptile-apps bot commented Mar 26, 2026

Greptile Summary

This PR enriches session registration with terminal location metadata — TTY path (resolved via spawnSync("tty")), tmux pane ID, iTerm2 session ID, and Apple Terminal session ID — and surfaces that information in the output of hunk session list and hunk session get. The new generic SessionTerminalMetadata / SessionTerminalLocation structure is cleanly extensible for future terminal integrations without needing additional top-level fields.\n\nKey changes:\n- New src/mcp/sessionTerminalMetadata.ts module captures and normalises metadata from TMUX_PANE, ITERM_SESSION_ID, TERM_SESSION_ID, and the tty command output into a structured, deduplication-aware location list.\n- ttyname() in sessionRegistration.ts gates on process.stdin.isTTY then shells out to tty to get the device path; called once at registration time so the synchronous spawnSync cost is negligible.\n- terminal is an optional field on both HunkSessionRegistration and ListedSession, making the change fully backward-compatible.\n- Formatting helpers in commands.ts integrate cleanly into both the list and detail output paths.\n- Test coverage is thorough across the new module, daemon state pass-through, and text/JSON formatters.

Confidence Score: 4/5

Safe to merge after addressing the locale-sensitive stdout check in ttyname(); all other concerns are non-blocking style suggestions.

The feature is additive and backward-compatible, test coverage is comprehensive, and the architecture is well-designed. One P1 concern: the "not a tty" string match in ttyname() is locale-dependent and should be replaced with a result.status !== 0 check. The two P2 items (trivial wrapper, explicit undefined spread) are cleanup suggestions that don't affect correctness.

src/mcp/sessionRegistration.ts — the ttyname() exit-code handling; src/session/commands.ts — the resolveSessionTerminal passthrough.

Important Files Changed

Filename Overview
src/mcp/sessionTerminalMetadata.ts New module that resolves TTY path, tmux pane, iTerm2, and Apple Terminal metadata into a generic location structure; logic is clean and well-encapsulated.
src/mcp/sessionRegistration.ts Adds ttyname() via spawnSync("tty") and wires terminal metadata into session registration; exit-code check is weaker than it should be.
src/mcp/types.ts Adds SessionTerminalLocation, SessionTerminalMetadata interfaces and optional terminal field to HunkSessionRegistration and ListedSession; changes are additive and backward-compatible.
src/mcp/daemonState.ts One-line pass-through of registration.terminal into ListedSession; straightforward and correct.
src/session/commands.ts Adds formatTerminalLines / formatTerminalLocation helpers and integrates them into both list and get formatters; resolveSessionTerminal is a redundant passthrough wrapper.
test/session-terminal-metadata.test.ts Good unit coverage for the new metadata resolver across presence/absence of env vars and TTY paths.
test/session-registration.test.ts Tests daemon state passes terminal metadata through to listed sessions, covering both populated and absent cases.
test/session-commands.test.ts Comprehensive tests for list/get text formatters and JSON output with terminal metadata; covers presence and absence paths.

Sequence Diagram

sequenceDiagram
    participant TUI as Hunk TUI process
    participant SR as sessionRegistration.ts
    participant STM as sessionTerminalMetadata.ts
    participant tty as tty command
    participant DS as HunkDaemonState
    participant CLI as hunk session list/get

    TUI->>SR: createSessionRegistration(bootstrap)
    SR->>SR: ttyname() — check process.stdin.isTTY
    SR->>tty: spawnSync("tty")
    tty-->>SR: /dev/ttys003
    SR->>STM: resolveSessionTerminalMetadata({ tty, env })
    STM->>STM: read TMUX_PANE, ITERM_SESSION_ID, TERM_SESSION_ID, TERM_PROGRAM
    STM-->>SR: SessionTerminalMetadata { program, locations[] }
    SR-->>DS: HunkSessionRegistration (with terminal field)
    DS->>DS: registerSession — stores terminal in ListedSession
    CLI->>DS: listSessions() / getSession()
    DS-->>CLI: ListedSession[] (with terminal)
    CLI->>CLI: formatTerminalLines() → text output
Loading

Reviews (1): Last reviewed commit: "refactor: generalize session terminal me..." | Re-trigger Greptile

Comment on lines +14 to +19
try {
const result = spawnSync("tty", [], { stdio: ["inherit", "pipe", "pipe"] });
const name = result.stdout?.toString().trim();
return name && !name.startsWith("not a tty") ? name : undefined;
} catch {
return undefined;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Prefer exit-code check over stdout string matching

The current guard against a failed tty invocation checks whether stdout starts with "not a tty", which is an English-locale assumption. On non-English systems the tty command may emit a localised error string that passes through this check, potentially storing a garbage path. Checking result.status === 0 is locale-independent and more robust:

Suggested change
try {
const result = spawnSync("tty", [], { stdio: ["inherit", "pipe", "pipe"] });
const name = result.stdout?.toString().trim();
return name && !name.startsWith("not a tty") ? name : undefined;
} catch {
return undefined;
const result = spawnSync("tty", [], { stdio: ["inherit", "pipe", "pipe"] });
if (result.status !== 0) {
return undefined;
}
const name = result.stdout?.toString().trim();
return name || undefined;

Comment on lines +394 to +396
function resolveSessionTerminal(session: ListedSession) {
return session.terminal;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Trivial passthrough wrapper can be inlined

resolveSessionTerminal returns session.terminal unchanged. The two call sites can access the field directly, avoiding an unnecessary indirection that suggests the function performs some computation. Replace both usages with session.terminal directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +58 to +62
return {
windowId: match.groups.window,
tabId: match.groups.tab,
paneId: match.groups.pane,
} satisfies Pick<SessionTerminalLocation, "windowId" | "tabId" | "paneId">;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Optional group spreads an explicit undefined property

When the hierarchical ID has no pane (e.g. "w1t2:ABC"), match.groups.pane is undefined. Spreading { windowId: "1", tabId: "2", paneId: undefined } into the location object creates an explicit paneId: undefined key. While JSON serialisation drops it, it may surprise code that iterates the object's own keys. Consider filtering out undefined entries before returning:

Suggested change
return {
windowId: match.groups.window,
tabId: match.groups.tab,
paneId: match.groups.pane,
} satisfies Pick<SessionTerminalLocation, "windowId" | "tabId" | "paneId">;
return Object.fromEntries(
Object.entries({
windowId: match.groups.window,
tabId: match.groups.tab,
paneId: match.groups.pane,
}).filter(([, v]) => v !== undefined),
) as Pick<SessionTerminalLocation, "windowId" | "tabId" | "paneId">;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Session list: include TTY and tmux pane metadata

1 participant